#!/usr/bin/perl

#
# Fri Oct 21 11:24:22 CEST 2011
# jakub.mikians@gmail.com
#

use warnings;
use strict;
use Getopt::Long;
use Data::Dumper;

use constant {
    DEF_RUN => ['date'],
    DEF_INCLUDE => ['*'],
    DEF_EXCLUDE => [],
    DEF_CONF => 'changerun.conf',
    DEF_DIR => '.',
    DEF_TIC => 1,
};

use constant {
    KEY_INCLUDE => 'include',
    KEY_EXCLUDE => 'exclude',
    KEY_TIC => 'tic',
    KEY_DIR => 'dir',
    KEY_RUN => 'run',
};

my $g_help = 0;
my $g_verbose = 0;
my ($g_run, $g_include, $g_exclude,
    $g_dir, $g_tic);

my $g_conf = DEF_CONF;

my $g_conf_opts = {};

sub main
{
    getOptions();
    my $optsconf = loadConfig();
    defaults($optsconf); 
    verbose("\n".Dumper($optsconf)."\n");
    applyConfig($optsconf);

    verbose("\n");
    my $state = fetchState();

    while(1) {
        tic();
        my $curr_state = fetchState();
        my $modified_file = statesDiffer($curr_state, $state);
        if (defined $modified_file) {
            verbose("MODIFIED: $modified_file\n");
            runCommand();
            $state = fetchState();
        }
        else {
            $state = $curr_state;
        }
    }
}

sub getOptions
{
    GetOptions(
        '--help' =>\$g_help,
        '--run=s@'  => \$g_run,
        '--include=s@' => \$g_include,
        '--exclude=s@' => \$g_exclude,
        '--conf=s' => \$g_conf,
        '--dir=s' => \$g_dir,
        '--verbose' => \$g_verbose,
        '--tic=i' => \$g_tic,
    ) or usage();

    usage() if $g_help;

}

sub loadConfig
{
    my $opts = {};
    if (-e $g_conf) {
        verbose("READING CONFIG: $g_conf\n");
        my $cnt = 0;
        for my $line (split /\n/, `cat $g_conf`) {
            $cnt++;
            chomp $line;
            $line =~ s/#.*//;
            next if shouldSkipLine($line);
            verbose("CONFIG LINE: $line\n");
            interpretConfigLine($opts, $line, $cnt);
        }
    }
    else {
        verbose("NO CONFIG: $g_conf\n");
    }
    return $opts;
}

sub shouldSkipLine
{
    my $line = shift;
    return 1 if $line =~ /^\s*$/;
}

sub interpretConfigLine
{
    my ($opts, $line, $cnt) = @_;

    if ($line =~ /\s*(\S+)\s*=\s*(.*)/)
    {
        my ($key, $val) = ($1, $2);
        $val =~ s/^\s*(.*?)\s*$/$1/;
        $opts->{$key} = [] if not exists $opts->{$key};

        if    (($key eq KEY_INCLUDE) or
               ($key eq KEY_EXCLUDE) or
               ($key eq KEY_RUN)) {
            push @{$opts->{$key}}, (split /\s*,\s*/, $val);
        }
        elsif (($key eq KEY_TIC) or
               ($key eq KEY_DIR)) {
            $opts->{$key} = $val;
        }
        else {
            print "error: bad config option in line $cnt: $line\n";
            exit;
        }
    }
    else {
        print "error: bad config line $cnt: $line\n";
        exit -1;
    }
}

sub defaults
{
    my $opts = shift;
    my %d = ( KEY_INCLUDE() => DEF_INCLUDE,
              KEY_EXCLUDE() => DEF_EXCLUDE,
              KEY_TIC() => DEF_TIC,
              KEY_DIR() => DEF_DIR,
              KEY_RUN() => DEF_RUN );
    for my $k(keys %d) {
        my $v = $d{$k};
        $opts->{$k} = $v if not defined $opts->{$k};
    }
}

sub applyConfig
{
    my $opts = shift;

    $g_run = $opts->{KEY_RUN()} if not defined $g_run;
    $g_include = $opts->{KEY_INCLUDE()} if not defined $g_include;
    $g_exclude = $opts->{KEY_EXCLUDE()} if not defined $g_exclude;
    $g_dir = $opts->{KEY_DIR()} if not defined $g_dir;
    $g_tic = $opts->{KEY_TIC()} if not defined $g_tic;

    verbose("--run: ". (defined $g_run ? join(" ",@{$g_run}) : '') ."\n");
    verbose("--include: ". (defined $g_include ? join(" ",@{$g_include}) : '') ."\n");
    verbose("--exclude: ". (defined $g_exclude ? join(" ",@{$g_exclude}) : '') ."\n");
    verbose("--conf: ".(defined $g_conf ? $g_conf : '') ."\n");
    verbose("--dir: ".(defined $g_dir ? $g_dir : '') ."\n");
    verbose("--tic: ".(defined $g_tic ? $g_tic : '') ."\n");

}

sub verbose
{
    my $msg = shift;
    print STDERR "$msg" if $g_verbose;
}



sub fetchState
{
    my $state = {};

    my @files = findFiles();
    for my $file (@files) {
        my $s = getStateForFile($file);
        $state->{$file} = $s;
#        verbose("$file:\t$s\n");
    }
    verbose("TOTAL FILES: " . scalar( keys %{$state}) ."\n");
    return $state;
}

sub findFiles
{
    my %hfiles;

    for my $pat (@{$g_include}) {
        my @ff = find($pat);
        for my $f (@ff) { $hfiles{$f} = 1 }
    }

    for my $pat (@{$g_exclude}) {
        my @ff = find($pat);
        for my $f (@ff) { delete $hfiles{$f} }
    }

    return keys %hfiles;
}

sub find
{
    my $pat = shift;
    my $cmd = "find $g_dir -type f -name '$pat'";
    my @ff = split /\n/, `$cmd`;
    return @ff;
}

sub getStateForFile
{
    my $filename = shift;
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
        $atime,$mtime,$ctime,$blksize,$blocks) = stat($filename);

    return $mtime;
}

sub tic
{
    sleep $g_tic;
}

sub statesDiffer
{
    my ($s1, $s2) = @_;

    my %keys;
    for my $k (keys %{$s1}) { $keys{$k} = 1 }
    for my $k (keys %{$s2}) { $keys{$k} = 1 }

    for my $k (keys %keys) {
        return $k if not defined $s1->{$k};
        return $k if not defined $s2->{$k};
        return $k if $s1->{$k} != $s2->{$k};
    }
    return undef;
}

sub runCommand
{
    for my $cmd (@{$g_run}) {
        verbose("RUNNING: $cmd\n");
        system $cmd;
    }
}

sub usage
{
    print<<END;
    changerun <OPTIONS>

    Options:
        -h --help               
            this help

        -r --run <commands>     
            commands to run, can be multiple
            Ex: --run 'echo test' --run 'make test' 

        -i --include <patterns> 
            files to include, can be multiple. Wildcards like * must be escaped, or enclosed in quotes. Patterns syntah is the same as for 'find' command
            Ex: -i '*.py' - '*.cpp'

        -e --exclude <patterns>
            exclude files matching patterns. Syntax similar to --include
            Ex: -e '*.swp' -e '*.log'

        -c --conf <file>
            configuration file. Default is changerun.conf
            Ex: --conf localconf.conf

        -d --dir <dir>
            directory where the files will be looked for.
            Ex: -d ../
            
        -t --tic <n>
            tic interval in seconds
            Ex: --tic 10

        -v --verbose
            verbose mode (debugging)

    Configuration file:
        See enclosed .changerun.conf for details

    Author:
        jakub.mikians<at>gmail.com
END

    exit;
}
main();

